package service

import (
	"context"
	"database/sql"
	"fmt"
	"path/filepath"
	"strings"

	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	"gitee.com/hp-huiw/my-go-core/config"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/security"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
)

const hashedPasswordPrefix = "{bcrypt}"

type UserService interface {
	MaskUsername(username string) string
	QueryAvatarFilename(ctx context.Context, userID string) (*string, error)
	ValidatePassword(ctx context.Context, username string, presentedPassword string) (bool, error)
	QueryStatus(ctx context.Context, username string) (*dto.UserStatusResponse, error)
	CheckOnUsername(ctx context.Context, username string) (bool, error)
	GetAvatarUrl(avatarFilename *string) string
	QueryDetails(ctx context.Context, accountGroupID string, idOrUsername string) (*dto.UserDetailResponse, error)
	QueryBasicInfo(ctx context.Context, accountGroupID string, usernames []string) ([]dto.UserBasicInfoResponse, error)
	QueryUsersNotInAccountGroup(ctx context.Context, accountGroupID string, keyword string, pageSize int64, pageIndex int64) ([]dto.UserBasicInfoResponse, error)
	QueryAccountGroupUsers(ctx context.Context, username string, pageSize int64, pageIndex int64) ([]dto.UserBasicInfoResponse, error)
	Count(ctx context.Context, accountGroupID string) (int64, error)
	Exists(ctx context.Context, userID string, accountGroupID string) (bool, error)
	RestoreAccountGroupByUserIDWithTx(ctx context.Context, tx *sql.Tx, userID string) error
	RestoreAccountGroupByAccountGroupIDWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
	UpdateAccountGroup(ctx context.Context, accountGroupID string, userID string) error
	UpdateCurrentUser(ctx context.Context, username string, fullName string, description *string) error
	UpdateRoles(ctx context.Context, accountGroupID string, data dto.UserRolesRequest) error
	UpdatePassword(ctx context.Context, currentUserAccountGroupID string, username string, password string) error
	UpdateAvatarFilename(ctx context.Context, id string, avatarFilename string) error
	InsertWithTx(ctx context.Context, tx *sql.Tx, model *dbmodel.User) error
	DeleteWithTx(ctx context.Context, tx *sql.Tx, id string) error
}

// NewUserService returns the instance of private member, userService, which implements UserService interface.
func NewUserService(db *sql.DB) UserService {
	return &userService{
		db:                      db,
		userRepository:          repository.NewUserRepository(db),
		accountGroupUserService: NewAccountGroupUserService(db),
		userRoleService:         NewUserRoleService(db),
	}
}

// MaskUsername masks username and avoids sensitive data revealing.
func (*userService) MaskUsername(username string) string {
	return fmt.Sprintf("%v****%v", username[0:3], username[len(username)-4:])
}

// QueryAvatarFilename queries user's avatar filename by user id.
func (s *userService) QueryAvatarFilename(ctx context.Context, userID string) (*string, error) {
	return s.userRepository.QueryAvatarFilename(ctx, userID)
}

// ValidatePassword validates if the presentedPassword matches the user stored password.
// Return: True means password matches. False means not.
func (s *userService) ValidatePassword(ctx context.Context, username string, presentedPassword string) (bool, error) {
	hashedPassword, err := s.userRepository.QueryPassword(ctx, username)

	if err != nil {
		return false, err
	}

	// Since Spring Security prefixes the hash with the encoding id. That's why we need to remove it in go.
	hashedPassword = strings.ReplaceAll(hashedPassword, hashedPasswordPrefix, "")

	return util.CheckPassword(presentedPassword, hashedPassword), nil
}

// QueryStatus queries user status data. Return user only contains ID, Username, Enabled, AccountExpired, AccountLocked, PasswordExpired fields.
func (s *userService) QueryStatus(ctx context.Context, username string) (*dto.UserStatusResponse, error) {
	query, err := s.userRepository.QueryStatus(ctx, username)

	if err != nil {
		return nil, err
	}

	return &dto.UserStatusResponse{
		UserCore: dto.UserCore{
			ID:       query.ID,
			Username: query.Username,
			Enabled:  &query.Enabled,
		},
		AccountExpired:  query.AccountExpired,
		AccountLocked:   query.AccountLocked,
		PasswordExpired: query.PasswordExpired,
	}, nil
}

// CheckOnUsername checks if the username already exists.
// Return: True means the username exists. False means not.
func (s *userService) CheckOnUsername(ctx context.Context, username string) (bool, error) {
	return s.userRepository.ExistsByUsername(ctx, username)
}

// GetAvatarUrl gets user avatar url based on its filename.
// Return: The full url of user avatar.
func (*userService) GetAvatarUrl(avatarFilename *string) string {
	config := config.ConfigLoader.GetAppConfig()

	if avatarFilename == nil {
		return config.Resource.DefaultAvatarUrl
	}

	avatarFileFullName := filepath.Join(config.Resource.Upload.RootFilePath, config.Resource.AvatarDirectoryName, *avatarFilename)

	if util.IsEmptyString(*avatarFilename) || !util.PathExists(avatarFileFullName) {
		return config.Resource.DefaultAvatarUrl
	}

	return fmt.Sprintf("%v/%v/%v", config.Resource.HttpBaseUrl, config.Resource.AvatarDirectoryName, *avatarFilename)
}

// QueryDetails queries the user details by id or username.
func (s *userService) QueryDetails(ctx context.Context, accountGroupID string, idOrUsername string) (*dto.UserDetailResponse, error) {
	query, err := s.userRepository.QueryDetails(ctx, accountGroupID, idOrUsername)

	if err != nil {
		return nil, err
	}

	isAccountGroupCreator, err := s.accountGroupUserService.ExistByAccountGroupAndUserAndIsAccountGroupCreator(ctx, query.AccountGroupID, query.ID)

	if err != nil {
		return nil, err
	}

	fullRoles, err := s.userRoleService.QueryFullRoles(ctx, query.AccountGroupID, query.ID)

	if err != nil {
		return nil, err
	}

	return &dto.UserDetailResponse{
		UserBasicInfoResponse: dto.UserBasicInfoResponse{
			UserCore: dto.UserCore{
				ID:       query.ID,
				Username: query.Username,
				Enabled:  query.Enabled,
			},
			FullName:              query.FullName,
			AvatarUrl:             s.GetAvatarUrl(query.AvatarFilename),
			IsAccountGroupCreator: &isAccountGroupCreator,
		},
		AccountGroupID: query.AccountGroupID,
		Description:    query.Description,
		FullRoles:      fullRoles,
		DateCreated:    query.DateCreated,
		LastUpdated:    query.LastUpdated,
	}, nil
}

// QueryBasicInfo tries to query user basic info. The original value returned If user not found.
func (s *userService) QueryBasicInfo(ctx context.Context, accountGroupID string, usernames []string) ([]dto.UserBasicInfoResponse, error) {
	uniqueUsernames := lo.Uniq(usernames)
	result := []dto.UserBasicInfoResponse{}

	if len(uniqueUsernames) > 0 {
		query, err := s.userRepository.QueryBasicInfo(ctx, accountGroupID, uniqueUsernames)

		if err != nil {
			return nil, err
		}

		for _, username := range uniqueUsernames {
			matched := lo.Filter(query, func(innerItem repository.UserBasicInfo, _ int) bool {
				return innerItem.Username == username
			})

			if len(matched) == 1 {
				matchedItem := matched[0]

				result = append(result, dto.UserBasicInfoResponse{
					UserCore: dto.UserCore{
						ID:       matchedItem.ID,
						Username: matchedItem.Username,
					},
					FullName:  matchedItem.FullName,
					AvatarUrl: s.GetAvatarUrl(matchedItem.AvatarFilename),
				})
			} else {
				result = append(result, dto.UserBasicInfoResponse{
					UserCore: dto.UserCore{
						Username: username,
					},
					FullName: username,
				})
			}
		}
	}

	return result, nil
}

// QueryUsersNotInAccountGroup queries users who are not in the provided account group.
func (s *userService) QueryUsersNotInAccountGroup(ctx context.Context, accountGroupID string, keyword string, pageSize int64, pageIndex int64) ([]dto.UserBasicInfoResponse, error) {
	query, err := s.userRepository.QueryUsersNotInAccountGroup(ctx, accountGroupID, keyword, pageSize, pageIndex)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.UserBasicInfo, _ int) dto.UserBasicInfoResponse {
		return dto.UserBasicInfoResponse{
			UserCore: dto.UserCore{
				ID:       item.ID,
				Username: item.Username,
			},
			FullName:  item.FullName,
			AvatarUrl: s.GetAvatarUrl(item.AvatarFilename),
		}
	})

	return result, nil
}

// QueryAccountGroupUsers queries account group users by user account group id.
func (s *userService) QueryAccountGroupUsers(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64) ([]dto.UserBasicInfoResponse, error) {
	query, err := s.userRepository.QueryAccountGroupUsers(ctx, accountGroupID, pageSize, pageIndex)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.UserBasicInfo, _ int) dto.UserBasicInfoResponse {
		isAdmin, _ := s.userRoleService.HasRole(ctx, accountGroupID, item.ID, security.RoleAccountGroupAdmin)

		return dto.UserBasicInfoResponse{
			UserCore: dto.UserCore{
				ID:       item.ID,
				Username: item.Username,
				Enabled:  item.Enabled,
			},
			FullName:              item.FullName,
			AvatarUrl:             s.GetAvatarUrl(item.AvatarFilename),
			IsAccountGroupCreator: item.IsAccountGroupCreator,
			IsAccountGroupAdmin:   &isAdmin,
		}
	})

	return result, nil
}

// Counts the account group user by accountGroupID.
func (s *userService) Count(ctx context.Context, accountGroupID string) (int64, error) {
	return s.userRepository.Count(ctx, accountGroupID)
}

// Checks existence of the account group user by user id and accountGroupID.
func (s *userService) Exists(ctx context.Context, userID string, accountGroupID string) (bool, error) {
	return s.userRepository.ExistsByUserIDAndAccountGroupID(ctx, userID, accountGroupID)
}

// Restores this user current account group to his/her own one.
func (s *userService) RestoreAccountGroupByUserIDWithTx(ctx context.Context, tx *sql.Tx, userID string) error {
	return s.userRepository.RestoreAccountGroupByUserIDWithTx(ctx, tx, userID)
}

// Account group with the input id is about to be deleted.
// All users with this account group id as current one will be restored to their own account group id.
func (s *userService) RestoreAccountGroupByAccountGroupIDWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	return s.userRepository.RestoreAccountGroupByAccountGroupIDWithTx(ctx, tx, accountGroupID)
}

// UpdateAccountGroup updates user account group id base on user id.
func (s *userService) UpdateAccountGroup(ctx context.Context, accountGroupID string, userID string) error {
	return s.userRepository.UpdateAccountGroup(ctx, accountGroupID, userID)
}

// Updates user info based on provided map data and username.
func (s *userService) UpdateCurrentUser(ctx context.Context, username string, fullName string, description *string) error {
	return s.userRepository.UpdateUserInfo(ctx, username, fullName, description)
}

// Updates user related roles.
func (s *userService) UpdateRoles(ctx context.Context, accountGroupID string, data dto.UserRolesRequest) error {
	userID := data.ID
	username := data.Username

	// Makes sure current user and parameter user belong to same user account group.
	// Makes sure account group owner's role cannot be changed.
	existByAccountGroupAndUser, err := s.accountGroupUserService.ExistByAccountGroupAndUser(ctx, accountGroupID, userID)

	if err != nil {
		return err
	}

	existByAccountGroupAndUserAndIsAccountGroupCreator, err := s.accountGroupUserService.ExistByAccountGroupAndUserAndIsAccountGroupCreator(ctx, accountGroupID, userID)

	if err != nil {
		return err
	}

	if existByAccountGroupAndUser && !existByAccountGroupAndUserAndIsAccountGroupCreator {
		fullRoles := data.FullRoles

		// If the user roles is set.
		if len(fullRoles) > 0 {
			// Gets the role id list from the posted data.
			roleIDs := make([]string, 0, len(fullRoles))

			for _, role := range fullRoles {
				roleIDs = append(roleIDs, role.ID)
			}

			// Queries user current role ids.
			currentRoleIDs, err := s.userRoleService.QueryRoleIDs(ctx, accountGroupID, userID)

			if err != nil {
				return err
			}

			// Gets all new added role ids.
			addedRoleIDs := []string{}

			for _, id := range roleIDs {
				if !lo.Contains(currentRoleIDs, id) {
					addedRoleIDs = append(addedRoleIDs, id)
				}
			}

			// Gets all deleted role ids.
			deletedRoleIDs := []string{}

			for _, id := range currentRoleIDs {
				if !lo.Contains(roleIDs, id) {
					deletedRoleIDs = append(deletedRoleIDs, id)
				}
			}

			// When updating user we also need to update user role.
			// That's why transaction is needed.
			tx, err := s.db.Begin()

			if err != nil {
				return err
			}

			defer tx.Rollback()

			// Adds new roles if needed.
			if len(addedRoleIDs) > 0 {
				if err := s.userRoleService.Add(ctx, tx, accountGroupID, userID, addedRoleIDs); err != nil {
					return err
				}
			}

			// Deletes new roles if needed.
			if len(deletedRoleIDs) > 0 {
				if err := s.userRoleService.Delete(ctx, tx, accountGroupID, userID, deletedRoleIDs); err != nil {
					return err
				}
			}

			// If role has been changed we need to make user re-login by deleting active tokens.
			if len(addedRoleIDs) > 0 || len(deletedRoleIDs) > 0 {
				if err := NewUserActiveTokenService(s.db).DeleteByUserAndAccountGroupWithTx(ctx, tx, username, accountGroupID); err != nil {
					return err
				}
			}

			return tx.Commit()
		}
	}

	return nil
}

// Updates the password for the user with provided username.
func (s *userService) UpdatePassword(ctx context.Context, currentUserAccountGroupID string, username string, password string) error {
	hashedPassword, err := util.HashPassword(password)

	// For compatible with SpringBoot hashed password.
	hashedPassword = hashedPasswordPrefix + hashedPassword

	if err != nil {
		return err
	}

	return s.userRepository.UpdatePassword(ctx, currentUserAccountGroupID, username, hashedPassword)
}

// UpdateAvatarFilename updates user avatar filename.
func (s *userService) UpdateAvatarFilename(ctx context.Context, id string, avatarFilename string) error {
	return s.userRepository.UpdateAvatarFilename(ctx, id, avatarFilename)
}

// InsertWithTx inserts user with transaction.
func (s *userService) InsertWithTx(ctx context.Context, tx *sql.Tx, model *dbmodel.User) error {
	hashedPassword, err := util.HashPassword(model.Password)

	if err != nil {
		return err
	}

	// For compatible with SpringBoot hashed password.
	hashedPassword = hashedPasswordPrefix + hashedPassword

	model.ID = util.GenerateUUIDWithoutDashes()
	model.Password = hashedPassword
	model.DateCreated = date.GetCurrentDate()
	model.LastUpdated = date.GetCurrentDate()

	return s.userRepository.InsertWithTx(ctx, tx, model)
}

// DeleteWithTx deletes user by id with transaction.
func (s *userService) DeleteWithTx(ctx context.Context, tx *sql.Tx, id string) error {
	return s.userRepository.DeleteWithTx(ctx, tx, id)
}

// // Gets user roles data from fullRoles. fullRoles must be map[string]any.
// func (*userService) getUserRoles(fullRoles []map[string]any) []*model.Role {
// 	roles := []*model.Role{}

// 	if fullRoles != nil {
// 		// Uses pkg.MapSlice() function to access data["fullRoles"] as slice.
// 		pkg.MapSlice(fullRoles, func(_ int, item map[string]any) model.Role {
// 			// Creates role data with provided id in map data.
// 			role := model.Role{
// 				BaseModel: model.BaseModel{ID: item["id"].(string)},
// 			}

// 			// Appends role to user.Roles field.
// 			roles = append(roles, &role)

// 			// The return data is not used here. Just to satisfy function signature.
// 			return role
// 		})
// 	}

// 	return roles
// }

// Container of UserService functions.
type userService struct {
	db                      *sql.DB
	userRepository          repository.UserRepository
	accountGroupUserService AccountGroupUserService
	userRoleService         UserRoleService
}
